home *** CD-ROM | disk | FTP | other *** search
/ Mac-Source 1994 July / Mac-Source_July_1994.iso / Other Langs / Tickle-4.0 (tcl) / tcl / extend / src / tclXfilescan.c < prev    next >
Encoding:
C/C++ Source or Header  |  1993-11-05  |  18.8 KB  |  612 lines  |  [TEXT/MPS ]

  1. /*
  2.  * tclXfilescan.c --
  3.  *
  4.  * Tcl file scanning: regular expression matching on lines of a file.  
  5.  * Implements awk.
  6.  *-----------------------------------------------------------------------------
  7.  * Copyright 1991-1993 Karl Lehenbauer and Mark Diekhans.
  8.  *
  9.  * Permission to use, copy, modify, and distribute this software and its
  10.  * documentation for any purpose and without fee is hereby granted, provided
  11.  * that the above copyright notice appear in all copies.  Karl Lehenbauer and
  12.  * Mark Diekhans make no representations about the suitability of this
  13.  * software for any purpose.  It is provided "as is" without express or
  14.  * implied warranty.
  15.  *-----------------------------------------------------------------------------
  16.  * $Id: tclXfilescan.c,v 2.8 1993/08/03 06:13:44 markd Exp $
  17.  *-----------------------------------------------------------------------------
  18.  */
  19.  
  20. #include "tclExtdInt.h"
  21. #ifndef macintosh
  22. #    include "regexp.h"
  23. #endif
  24.  
  25. /*
  26.  * A scan context describes a collection of match patterns and commands,
  27.  * along with a match default command to apply to a file on a scan.
  28.  */
  29.  
  30. #define CONTEXT_A_CASE_INSENSITIVE_FLAG 2
  31. #define MATCH_CASE_INSENSITIVE_FLAG 4
  32.  
  33. typedef struct matchDef_t {
  34.     Tcl_regexp          regExpInfo;
  35.     char               *command;
  36.     struct matchDef_t  *nextMatchDefPtr;
  37.     short               matchflags;
  38. } matchDef_t;
  39.  
  40. typedef struct scanContext_t {
  41.     matchDef_t  *matchListHead;
  42.     matchDef_t  *matchListTail;
  43.     char        *defaultAction;
  44.     short        flags;
  45. } scanContext_t;
  46.  
  47. /*
  48.  * Global data structure, pointer to by clientData.
  49.  */
  50.  
  51. typedef struct {
  52.     void_pt         tblHdrPtr;     /* Scan context handle table           */
  53.     char            curName [16];  /* Current context name.               */ 
  54. } scanGlob_t;
  55.  
  56. /*
  57.  * Prototypes of internal functions.
  58.  */
  59. static int
  60. CleanUpContext _ANSI_ARGS_((scanGlob_t     *scanGlobPtr,
  61.                             scanContext_t  *contextPtr));
  62.  
  63. static int
  64. CreateScanContext _ANSI_ARGS_((Tcl_Interp  *interp,
  65.                                scanGlob_t  *scanGlobPtr));
  66.  
  67. static int
  68. SelectScanContext _ANSI_ARGS_((Tcl_Interp  *interp,
  69.                                scanGlob_t  *scanGlobPtr,
  70.                                char        *contextHandle));
  71.  
  72. static int
  73. Tcl_Delete_scancontextCmd _ANSI_ARGS_((Tcl_Interp  *interp,
  74.                                        scanGlob_t  *scanGlobPtr,
  75.                                        char        *contextHandle));
  76.  
  77. static int
  78. SetMatchVar _ANSI_ARGS_((Tcl_Interp *interp,
  79.                          char       *fileLine,
  80.                          long        scanLineNum,
  81.                          FILE       *filePtr));
  82.  
  83. static int
  84. SetSubMatchVar _ANSI_ARGS_((Tcl_Interp       *interp,
  85.                             char             *fileLine,
  86.                             Tcl_SubMatchInfo  subMatchInfo));
  87.  
  88. static void
  89. FileScanCleanUp _ANSI_ARGS_((ClientData  clientData,
  90.                              Tcl_Interp *interp));
  91.  
  92.  
  93. /*
  94.  *-----------------------------------------------------------------------------
  95.  * CleanUpContext --
  96.  *
  97.  *   Release all resources allocated to the specified scan context entry.  The
  98.  *  entry itself is not released.
  99.  *-----------------------------------------------------------------------------
  100.  */
  101. static int
  102. CleanUpContext (scanGlobPtr, contextPtr)
  103.     scanGlob_t    *scanGlobPtr;
  104.     scanContext_t *contextPtr;
  105. {
  106.     matchDef_t  *matchPtr, *oldMatchPtr;
  107.  
  108.     for (matchPtr = contextPtr->matchListHead; matchPtr != NULL;) {
  109.         Tcl_RegExpClean (&matchPtr->regExpInfo);
  110.         if (matchPtr->command != NULL)
  111.             ckfree(matchPtr->command);
  112.         oldMatchPtr = matchPtr;
  113.         matchPtr = matchPtr->nextMatchDefPtr;
  114.         ckfree ((char *) oldMatchPtr);
  115.         }
  116.     contextPtr->matchListHead = NULL;
  117.     contextPtr->matchListTail = NULL;
  118.  
  119.     if (contextPtr->defaultAction != NULL) {
  120.         ckfree(contextPtr->defaultAction);
  121.         contextPtr->defaultAction = NULL;
  122.     }
  123. }
  124.  
  125. /*
  126.  *-----------------------------------------------------------------------------
  127.  * CreateScanContext --
  128.  *
  129.  *   Create a new scan context, implements the subcommand:
  130.  *         scancontext create
  131.  *-----------------------------------------------------------------------------
  132.  */
  133. static int
  134. CreateScanContext (interp, scanGlobPtr)
  135.     Tcl_Interp  *interp;
  136.     scanGlob_t  *scanGlobPtr;
  137. {
  138.     scanContext_t *contextPtr;
  139.  
  140.     contextPtr = (scanContext_t *) Tcl_HandleAlloc (scanGlobPtr->tblHdrPtr, 
  141.                                                     scanGlobPtr->curName);
  142.     contextPtr->flags = 0;
  143.     contextPtr->matchListHead = NULL;
  144.     contextPtr->matchListTail = NULL;
  145.     contextPtr->defaultAction = NULL;
  146.  
  147.     Tcl_SetResult (interp, scanGlobPtr->curName, TCL_STATIC);
  148.     return TCL_OK;
  149. }
  150.  
  151. /*
  152.  *-----------------------------------------------------------------------------
  153.  * DeleteScanContext --
  154.  *
  155.  *   Deletes the specified scan context, implements the subcommand:
  156.  *         scancontext delete contexthandle
  157.  *-----------------------------------------------------------------------------
  158.  */
  159. static int
  160. DeleteScanContext (interp, scanGlobPtr, contextHandle)
  161.     Tcl_Interp  *interp;
  162.     scanGlob_t  *scanGlobPtr;
  163.     char        *contextHandle;
  164. {
  165.     scanContext_t *contextPtr;
  166.  
  167.     if ((contextPtr = Tcl_HandleXlate (interp, scanGlobPtr->tblHdrPtr, 
  168.                                        contextHandle)) == NULL)
  169.         return TCL_ERROR;
  170.  
  171.     CleanUpContext (scanGlobPtr, contextPtr);
  172.     Tcl_HandleFree (scanGlobPtr->tblHdrPtr, contextPtr);
  173.  
  174.     return TCL_OK;
  175. }
  176.  
  177. /*
  178.  *-----------------------------------------------------------------------------
  179.  * Tcl_ScancontextCmd --
  180.  *
  181.  *   Implements the TCL scancontext Tcl command, which has the following forms:
  182.  *         scancontext create
  183.  *         scancontext delete
  184.  *-----------------------------------------------------------------------------
  185.  */
  186. static int
  187. Tcl_ScancontextCmd (clientData, interp, argc, argv)
  188.     char       *clientData;
  189.     Tcl_Interp *interp;
  190.     int         argc;
  191.     char      **argv;
  192. {
  193.     scanGlob_t  *scanGlobPtr = (scanGlob_t *) clientData;
  194.  
  195.     if (argc < 2) {
  196.         Tcl_AppendResult (interp, tclXWrongArgs, argv [0], " option",
  197.                           (char *) NULL);
  198.         return TCL_ERROR;
  199.     }
  200.     /*
  201.      * Create a new scan context.
  202.      */
  203.     if (STREQU (argv [1], "create")) {
  204.         if (argc != 2) {
  205.             Tcl_AppendResult (interp, tclXWrongArgs, argv [0], " create",
  206.                               (char *) NULL);
  207.             return TCL_ERROR;
  208.         }
  209.         return CreateScanContext (interp, scanGlobPtr);        
  210.     }
  211.     
  212.     /*
  213.      * Delete a scan context.
  214.      */
  215.     if (STREQU (argv [1], "delete")) {
  216.         if (argc != 3) {
  217.             Tcl_AppendResult (interp, tclXWrongArgs, argv [0],
  218.                               "delete contexthandle", (char *) NULL);
  219.             return TCL_ERROR;
  220.         }
  221.         return DeleteScanContext (interp, scanGlobPtr, argv [2]);
  222.     }
  223.     
  224.     Tcl_AppendResult (interp, "invalid argument, expected one of: ",
  225.                       "create or delete", (char *) NULL);
  226.     return TCL_ERROR;
  227. }
  228.  
  229. /*
  230.  *-----------------------------------------------------------------------------
  231.  * Tcl_ScanmatchCmd --
  232.  *
  233.  *   Implements the TCL command:
  234.  *         scanmatch ?-nocase? contexthandle ?regexp? command
  235.  *
  236.  *   This uses both Boyer_Moore and regular expressions matching.
  237.  *-----------------------------------------------------------------------------
  238.  */
  239. static int
  240. Tcl_ScanmatchCmd (clientData, interp, argc, argv)
  241.     char       *clientData;
  242.     Tcl_Interp *interp;
  243.     int         argc;
  244.     char      **argv;
  245. {
  246.     scanGlob_t     *scanGlobPtr = (scanGlob_t *) clientData;
  247.     scanContext_t  *contextPtr;
  248.     char           *result;
  249.     matchDef_t     *newmatch;
  250.     int             compFlags = REXP_BOTH_ALGORITHMS;
  251.     int             firstArg = 1;
  252.  
  253.     if (argc < 3)
  254.         goto argError;
  255.     if (STREQU (argv[1], "-nocase")) {
  256.         compFlags |= REXP_NO_CASE;
  257.         firstArg = 2;
  258.     }
  259.       
  260.     /*
  261.      * If firstArg == 2 (-nocase), the both a regular expression and a command
  262.      * string must be specified, otherwise the regular expression is optional.
  263.      */
  264.     if (((firstArg == 2) && (argc != 5)) || ((firstArg == 1) && (argc > 4)))
  265.         goto argError;
  266.  
  267.     if ((contextPtr = Tcl_HandleXlate (interp, scanGlobPtr->tblHdrPtr, 
  268.                                        argv [firstArg])) == NULL)
  269.         return TCL_ERROR;
  270.  
  271.     /*
  272.      * Handle the default case (no regular expression).
  273.      */
  274.     if (argc == 3) {
  275.         if (contextPtr->defaultAction) {
  276.             Tcl_AppendResult (interp, argv [0], ": default match already ",
  277.                               "specified in this scan context", (char *) NULL);
  278.             return TCL_ERROR;
  279.         }
  280.         contextPtr->defaultAction = ckstrdup (argv [2]);
  281.  
  282.         return TCL_OK;
  283.     }
  284.  
  285.     /*
  286.      * Add a regular expression to the context.
  287.      */
  288.  
  289.     newmatch = (matchDef_t *) ckalloc(sizeof (matchDef_t));
  290.     newmatch->matchflags = 0;
  291.  
  292.     if (compFlags & REXP_NO_CASE) {
  293.         newmatch->matchflags |= MATCH_CASE_INSENSITIVE_FLAG;
  294.         contextPtr->flags |= CONTEXT_A_CASE_INSENSITIVE_FLAG;
  295.     }
  296.  
  297.     if (Tcl_RegExpCompile (interp, &newmatch->regExpInfo, argv [firstArg + 1], 
  298.                            compFlags) != TCL_OK) {
  299.         ckfree ((char *) newmatch);
  300.         return (TCL_ERROR);
  301.     }
  302.  
  303.     newmatch->command = ckstrdup (argv [firstArg + 2]);
  304.  
  305.     /*
  306.      * Link in the new match.
  307.      */
  308.     newmatch->nextMatchDefPtr = NULL;
  309.     if (contextPtr->matchListHead == NULL)
  310.         contextPtr->matchListHead = newmatch;
  311.     else
  312.         contextPtr->matchListTail->nextMatchDefPtr = newmatch;
  313.     contextPtr->matchListTail = newmatch;
  314.  
  315.     return TCL_OK;
  316.  
  317. argError:
  318.     Tcl_AppendResult (interp, tclXWrongArgs, argv [0],
  319.                       " ?-nocase? contexthandle ?regexp? command",
  320.                       (char *) NULL);
  321.     return TCL_ERROR;
  322. }
  323.  
  324. /*
  325.  *-----------------------------------------------------------------------------
  326.  * SetMatchVar --
  327.  *
  328.  *   Sets the TCL array variable matchInfo to contain information 
  329.  * about the line that is matched.
  330.  *-----------------------------------------------------------------------------
  331.  */
  332. static int
  333. SetMatchVar (interp, fileLine, scanLineNum, filePtr)
  334.     Tcl_Interp *interp;
  335.     char       *fileLine;
  336.     long        scanLineNum;
  337.     FILE       *filePtr;
  338. {
  339.     int  matchOffset;
  340.     char buf [32];
  341.  
  342.     Tcl_UnsetVar (interp, "matchInfo", 0);
  343.  
  344.     matchOffset = ftell (filePtr) - (strlen (fileLine) + 1);
  345.  
  346.     if (Tcl_SetVar2 (interp, "matchInfo", "line", fileLine, 
  347.                      TCL_LEAVE_ERR_MSG) == NULL)
  348.         return TCL_ERROR;
  349.  
  350.     sprintf (buf, "%ld", matchOffset);
  351.     if (Tcl_SetVar2 (interp, "matchInfo", "offset", buf,
  352.                      TCL_LEAVE_ERR_MSG) == NULL)
  353.         return TCL_ERROR;
  354.  
  355.     sprintf (buf, "%ld", scanLineNum);
  356.     if (Tcl_SetVar2 (interp, "matchInfo", "linenum", buf,
  357.                      TCL_LEAVE_ERR_MSG) == NULL)
  358.         return TCL_ERROR;
  359.  
  360.     sprintf (buf, "file%d", fileno (filePtr));
  361.     if (Tcl_SetVar2 (interp, "matchInfo", "handle", buf, 
  362.                      TCL_LEAVE_ERR_MSG) == NULL)
  363.         return TCL_ERROR;
  364.     return TCL_OK;
  365. }
  366.  
  367. /*
  368.  *-----------------------------------------------------------------------------
  369.  * SetSubMatchVar --
  370.  *
  371.  *   Sets the TCL array variable matchInfo entries to describe the matching
  372.  * subexpressions.
  373.  *-----------------------------------------------------------------------------
  374.  */
  375. static int
  376. SetSubMatchVar (interp, fileLine, subMatchInfo)
  377.     Tcl_Interp       *interp;
  378.     char             *fileLine;
  379.     Tcl_SubMatchInfo  subMatchInfo;
  380. {
  381.     int  idx, start, end;
  382.     char key [32], buf [32], *varPtr, holdChar;
  383.     
  384.     for (idx = 0; subMatchInfo [idx].start >= 0; idx++) {
  385.         start = subMatchInfo [idx].start;
  386.         end = subMatchInfo [idx].end;
  387.  
  388.         sprintf (key, "subindex%d", idx);
  389.         sprintf (buf, "%d %d", start, end);
  390.         varPtr = Tcl_SetVar2 (interp, "matchInfo", key, buf,
  391.                               TCL_LEAVE_ERR_MSG);
  392.         if (varPtr == NULL)
  393.             return TCL_ERROR;
  394.  
  395.         sprintf (key, "submatch%d", idx);
  396.         holdChar = fileLine [end + 1];
  397.         fileLine [end + 1] = '\0';
  398.         varPtr = Tcl_SetVar2 (interp, "matchInfo", key,
  399.                               fileLine + start,
  400.                               TCL_LEAVE_ERR_MSG);
  401.         fileLine [end + 1] = holdChar;
  402.         if (varPtr == NULL)
  403.             return TCL_ERROR;
  404.     }
  405.     return TCL_OK;
  406. }
  407.  
  408. /*
  409.  *-----------------------------------------------------------------------------
  410.  * Tcl_ScanfileCmd --
  411.  *
  412.  *   Implements the TCL command:
  413.  *        scanfile contexthandle filehandle
  414.  *-----------------------------------------------------------------------------
  415.  */
  416. static int
  417. Tcl_ScanfileCmd (clientData, interp, argc, argv)
  418.     char       *clientData;
  419.     Tcl_Interp *interp;
  420.     int         argc;
  421.     char      **argv;
  422. {
  423.     scanGlob_t       *scanGlobPtr = (scanGlob_t *) clientData;
  424.     scanContext_t    *contextPtr;
  425.     Tcl_DString       dynBuf, lowerDynBuf;
  426.     FILE             *filePtr;
  427.     matchDef_t       *matchPtr;
  428.     int               result, matchedAtLeastOne, status, storedThisLine;
  429.     long              scanLineNum = 0;
  430.     char             *fileHandle;
  431.     Tcl_SubMatchInfo  subMatchInfo;
  432.  
  433.     if ((argc < 2) || (argc > 3)) {
  434.         Tcl_AppendResult (interp, tclXWrongArgs, argv [0], 
  435.                           " contexthandle filehandle", (char *) NULL);
  436.         return TCL_ERROR;
  437.     }
  438.  
  439.     if ((contextPtr = Tcl_HandleXlate (interp, scanGlobPtr->tblHdrPtr, 
  440.                                        argv [1])) == NULL)
  441.         return TCL_ERROR;
  442.  
  443.     if (Tcl_GetOpenFile (interp, argv [2],
  444.                          FALSE,  /* Read access  */
  445.                          TRUE,   /* Check access */
  446.                          &filePtr) != TCL_OK)
  447.             return TCL_ERROR;
  448.  
  449.     if (contextPtr->matchListHead == NULL) {
  450.         Tcl_AppendResult (interp, "no patterns in current scan context",
  451.                           (char *) NULL);
  452.         return TCL_ERROR;
  453.     }
  454.  
  455.     Tcl_DStringInit (&dynBuf);
  456.     Tcl_DStringInit (&lowerDynBuf);
  457.  
  458.     result = TCL_OK;
  459.     while (TRUE) {
  460.         Tcl_DStringFree (&dynBuf);
  461.         status = Tcl_DStringGets (filePtr, &dynBuf) ;
  462.  
  463.         if (status == TCL_ERROR) {
  464.             interp->result = Tcl_PosixError (interp);
  465.             result = TCL_ERROR;
  466.             goto scanExit;
  467.         }
  468.         if (status == TCL_BREAK)
  469.             goto scanExit;  /* EOF */
  470.  
  471.         scanLineNum++;
  472.         storedThisLine = FALSE;
  473.         matchedAtLeastOne = FALSE;
  474.  
  475.         if (contextPtr->flags & CONTEXT_A_CASE_INSENSITIVE_FLAG) {
  476.             Tcl_DStringFree (&lowerDynBuf);
  477.             Tcl_DStringAppend (&lowerDynBuf, dynBuf.string, -1);
  478.             Tcl_DownShift (lowerDynBuf.string, lowerDynBuf.string);
  479.         }
  480.  
  481.         for (matchPtr = contextPtr->matchListHead; matchPtr != NULL; 
  482.                  matchPtr = matchPtr->nextMatchDefPtr) {
  483.  
  484.             if (!Tcl_RegExpExecute (interp,
  485.                                     &matchPtr->regExpInfo,
  486.                                     dynBuf.string,
  487.                                     lowerDynBuf.string,
  488.                                     subMatchInfo))
  489.                 continue;  /* Try next match pattern */
  490.  
  491.             matchedAtLeastOne = TRUE;
  492.             if (!storedThisLine) {
  493.                 result = SetMatchVar (interp,
  494.                                       dynBuf.string,
  495.                                       scanLineNum,
  496.                                       filePtr);
  497.                 if (result != TCL_OK)
  498.                     goto scanExit;
  499.                 storedThisLine = TRUE;
  500.             }
  501.             result = SetSubMatchVar (interp, dynBuf.string, subMatchInfo);
  502.             if (result != TCL_OK)
  503.                 goto scanExit;
  504.  
  505.             result = Tcl_Eval(interp, matchPtr->command);
  506.             if (result == TCL_ERROR) {
  507.                 Tcl_AddErrorInfo (interp, 
  508.                     "\n    while executing a match command");
  509.                 goto scanExit;
  510.             }
  511.             if (result == TCL_CONTINUE) {
  512.                 /* 
  513.                  * Don't process any more matches for this line.
  514.                  */
  515.                 goto matchLineExit;
  516.             }
  517.             if (result == TCL_BREAK) {
  518.                 /*
  519.                  * Terminate scan.
  520.                  */
  521.                 result = TCL_OK;
  522.                 goto scanExit;
  523.             }
  524.         }
  525.  
  526.         matchLineExit:
  527.         /*
  528.          * Process default action if required.
  529.          */
  530.         if ((contextPtr->defaultAction != NULL) && (!matchedAtLeastOne)) {
  531.             result = SetMatchVar (interp,
  532.                                   dynBuf.string,
  533.                                   scanLineNum,
  534.                                   filePtr);
  535.             if (result != TCL_OK)
  536.                 goto scanExit;
  537.  
  538.             result = Tcl_Eval (interp, contextPtr->defaultAction);
  539.             if (result == TCL_ERROR)
  540.                 Tcl_AddErrorInfo (interp, 
  541.                     "\n    while executing a match default command");
  542.             if (result == TCL_BREAK)
  543.                 goto scanExit;
  544.         }
  545.         
  546.     }
  547. scanExit:
  548.     Tcl_DStringFree (&dynBuf);
  549.     Tcl_DStringFree (&lowerDynBuf);
  550.     if (result == TCL_ERROR)
  551.         return TCL_ERROR;
  552.     return TCL_OK;
  553. }
  554.  
  555. /*
  556.  *-----------------------------------------------------------------------------
  557.  * FileScanCleanUp --
  558.  *
  559.  *    Called when the interpreter is deleted to cleanup all filescan
  560.  * resources
  561.  *-----------------------------------------------------------------------------
  562.  */
  563. static void
  564. FileScanCleanUp (clientData, interp)
  565.     ClientData  clientData;
  566.     Tcl_Interp *interp;
  567. {
  568.     scanGlob_t    *scanGlobPtr = (scanGlob_t *) clientData;
  569.     scanContext_t *contextPtr;
  570.     int            walkKey;
  571.     
  572.     walkKey = -1;
  573.     while ((contextPtr = Tcl_HandleWalk (scanGlobPtr->tblHdrPtr, 
  574.             &walkKey)) != NULL)
  575.         CleanUpContext (scanGlobPtr, contextPtr);
  576.  
  577.     Tcl_HandleTblRelease (scanGlobPtr->tblHdrPtr);
  578.     ckfree ((char *) scanGlobPtr);
  579. }
  580.  
  581. /*
  582.  *-----------------------------------------------------------------------------
  583.  *  Tcl_InitFilescan --
  584.  *
  585.  *    Initialize the TCL file scanning facility..
  586.  *-----------------------------------------------------------------------------
  587.  */
  588. void
  589. Tcl_InitFilescan (interp)
  590.     Tcl_Interp *interp;
  591. {
  592.     scanGlob_t  *scanGlobPtr;
  593.     void_pt      fileCbTblPtr;
  594.  
  595.     scanGlobPtr = (scanGlob_t *) ckalloc (sizeof (scanGlob_t));
  596.     scanGlobPtr->tblHdrPtr = 
  597.         Tcl_HandleTblInit ("context", sizeof (scanContext_t), 5);
  598.  
  599.     Tcl_CallWhenDeleted (interp, FileScanCleanUp, (ClientData) scanGlobPtr);
  600.  
  601.     /*
  602.      * Initialize the commands.
  603.      */
  604.     Tcl_CreateCommand (interp, "scanfile", Tcl_ScanfileCmd, 
  605.                        (ClientData) scanGlobPtr, (void (*)()) NULL);
  606.     Tcl_CreateCommand (interp, "scanmatch", Tcl_ScanmatchCmd, 
  607.                        (ClientData) scanGlobPtr, (void (*)()) NULL);
  608.     Tcl_CreateCommand (interp, "scancontext", Tcl_ScancontextCmd,
  609.                        (ClientData) scanGlobPtr, (void (*)()) NULL);
  610. }
  611.  
  612.